package model;

import net.beadsproject.beads.core.AudioContext;
import net.beadsproject.beads.core.Bead;
import net.beadsproject.beads.core.UGen;
import net.beadsproject.beads.ugens.Clock;
import net.beadsproject.beads.ugens.Envelope;
import net.beadsproject.beads.ugens.Gain;

/**
 * The Player plays the piece and outputs sound.
 */
public class Player {

	private boolean playing;
	private Piece piece;
	private Sequence selectedSequence;
	private int time;
	private AudioContext audioContext;
	private Clock clock;
	private boolean playOnce;

	public Player(Piece playPiece) {
		this(playPiece, new AudioContext());
	}

	public Player(Piece playPiece, AudioContext audioContext) {
		this.time = 0;
		this.playing = false;
		this.playOnce = false;
		this.piece = playPiece;
		this.selectedSequence = null;
		this.audioContext = audioContext;

		//Set up the clock to match the piece's tempo.
		this.clock = new Clock(audioContext, (2000-piece.getTempo())*5);
		this.clock.addMessageListener(new Bead() {
			@Override
			public void messageReceived(Bead message) {
				if (!playing) return; //Do nothing if the piece is not playing

				if (hasSelectedSequence()) {
					playSelectedSequence(); //Only play the selected sequence
				} else {
					playPiece(); //Play all the sequences in the piece.
				}
			}
		});

		audioContext.out.addDependent(clock);
	}

	public AudioContext getAudioContext() {
		return this.audioContext;
	}

	public void startAudioContext() {
		this.audioContext.start();
	}

	public void stopAudioContext() {
		this.audioContext.stop();
	}

	public void playSequence(Sequence sequence) {
		playNote(sequence, sequence.getNoteAt(time));
	}

	public void playSelectedSequence() {
		playSequence(selectedSequence);
	}

	public void playPiece() {
		for (Sequence sequence : piece.getSequences()) {
			playSequence(sequence);
		}
	}

	public void playNote(Sequence sequence, int note) {
		if (note < 1 || note > 9) return; //Note is out of bounds

		//Play the note using the sequence's instrument.
		UGen ugen = sequence.getInstrument().getSampleAtNote(audioContext, note);

		//Adjust the volume to the sequence's volumne.
		Gain gain = new Gain(audioContext, 1, new Envelope(
			audioContext, (float)sequence.getVolume()/100f)
		);
		gain.addInput(ugen);
		gain.setKillListener(ugen);
		ugen = gain;

		/* This seems to cause a huge stack of inputs to the AudioContext
		 * which causes the program to grind to a near-halt. Disabled for now.
		Reverb reverb = new Reverb(audioContext);
		reverb.addInput(ugen);
		reverb.setKillListener(ugen);
		ugen = reverb;
		*/

		audioContext.out.addInput(ugen);
	}

	public void setPiece(Piece piece) {
		this.piece = piece;
	}

	public Piece getPiece() {
		return piece;
	}

	public Clock getClock() {
		return clock;
	}

	public void selectSequence(Sequence sequence) {
		this.selectedSequence = sequence;
	}

	public void unselectSequence() {
		selectedSequence = null;
	}

	public boolean hasSelectedSequence() {
		return selectedSequence != null;
	}

	public Sequence getSelectedSequence() {
		if (!hasSelectedSequence()) {
			throw new NoSequenceSelectedException();
		} else {
			return selectedSequence;
		}
	}

	public boolean isPlaying() {
		return playing;
	}

	public void pause() {
		playing = false;
	}

	public void startPlaying() {
		playing = true;
	}

	/**
	 * Should the player play the piece once and stop at the end
	 * instead of looping?
	 */
	public void playOnce(boolean playOnce) {
		this.playOnce = playOnce;
	}

	/**
	 * Pauses this player and sets the time back to zero.
	 */
	public void stop() {
		pause();
		setTime(0);
	}

	public void setTime(int time) {
		this.time = time;
		checkTime();
	}

	public int getTime() {
		return time;
	}

	public void incrementTime() {
		time++;
		checkTime();
	}

	/**
	 * Checks to see if the time has passed the length of the piece.
	 * If so, the time will be set back to zero.
	 */
	public void checkTime() {
		if (time >= piece.getLength()) {
			time = 0;
			if(playOnce) this.pause();
		}
	}

	/**
	 * A higher tempo value means a lower update interval for the clock.
	 * @param tempo
	 */
	public void setTempo(int tempo) {
		piece.setTempo(tempo);
		clock.getIntervalEnvelope().setValue((2000-tempo)*5);
	}

	public class NoSequenceSelectedException extends RuntimeException {
		public NoSequenceSelectedException() {
			super("The Player has no Sequence selected to retrieve");
		}
	}
}
